Skip to content

Conversation

@Donnerbart
Copy link
Contributor

The behavior of HttpServer.createContext() was changed between Java 11 and 21. In the current JDKs the server doesn't allow to register multiple contexts with the same path. So if you configure

HTTPServer.builder()
            .port(0)
            .registry(registry)
            .metricsHandlerPath("/")
            .buildAndStart()

you get the following exception:

java.lang.IllegalArgumentException: cannot add context to list
	at jdk.httpserver/sun.net.httpserver.ContextList.add(ContextList.java:37)
	at jdk.httpserver/sun.net.httpserver.ServerImpl.createContext(ServerImpl.java:306)
	at jdk.httpserver/sun.net.httpserver.HttpServerImpl.createContext(HttpServerImpl.java:69)
	at jdk.httpserver/sun.net.httpserver.HttpServerImpl.createContext(HttpServerImpl.java:34)
	at io.prometheus.metrics.exporter.httpserver.HTTPServer.registerHandler(HTTPServer.java:111)

This PR fixes the issue by skipping the default handler registration in case the metrics are configured for the root path.

I also improved the unit test so we can properly assert expectedStatusCode and expectedBody. With this improvement we can be sure that an endpoint actually returns the expected handler content, not just a matching status code.

@zeitlinger
Copy link
Member

it looks as though this is not backwards compatible - maybe add a setting to opt in?

@Donnerbart
Copy link
Contributor Author

Donnerbart commented Jan 13, 2026

What exactly do you think breaks the backwards compatibility? With Java 11 it should already behave exactly like this (replacing the previously registered default handler). And with a newer Java runtime the current implementation doesn't work at all.

@Donnerbart Donnerbart force-pushed the bugfix/fix-exception-when-custom-root-metrics-path branch 2 times, most recently from 7745eb9 to b5ecdd1 Compare January 19, 2026 11:54
Signed-off-by: David Sondermann <david.sondermann@hivemq.com>
@Donnerbart Donnerbart force-pushed the bugfix/fix-exception-when-custom-root-metrics-path branch from b5ecdd1 to 2377a3f Compare January 20, 2026 08:11
@zeitlinger
Copy link
Member

What exactly do you think breaks the backwards compatibility?

That registration would not work the first time

What about this change?

Index: prometheus-metrics-exporter-httpserver/src/main/java/io/prometheus/metrics/exporter/httpserver/HTTPServer.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/prometheus-metrics-exporter-httpserver/src/main/java/io/prometheus/metrics/exporter/httpserver/HTTPServer.java b/prometheus-metrics-exporter-httpserver/src/main/java/io/prometheus/metrics/exporter/httpserver/HTTPServer.java
--- a/prometheus-metrics-exporter-httpserver/src/main/java/io/prometheus/metrics/exporter/httpserver/HTTPServer.java	(revision 7f93e015dc64160204b51d13aebf75c83044a364)
+++ b/prometheus-metrics-exporter-httpserver/src/main/java/io/prometheus/metrics/exporter/httpserver/HTTPServer.java	(date 1768917816428)
@@ -9,6 +9,9 @@
 import com.sun.net.httpserver.HttpsServer;
 import io.prometheus.metrics.config.PrometheusProperties;
 import io.prometheus.metrics.model.registry.PrometheusRegistry;
+
+import javax.annotation.Nullable;
+import javax.security.auth.Subject;
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
@@ -21,8 +24,6 @@
 import java.util.concurrent.SynchronousQueue;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
-import javax.annotation.Nullable;
-import javax.security.auth.Subject;
 
 /**
  * Expose Prometheus metrics using a plain Java HttpServer.
@@ -66,11 +67,21 @@
     this.server = httpServer;
     this.executorService = executorService;
     String metricsPath = getMetricsPath(metricsHandlerPath);
+    try {
+      server.removeContext("/");
+    } catch (IllegalArgumentException e) {
+      // context "/" not registered yet, ignore
+    }
     registerHandler(
         "/",
         defaultHandler == null ? new DefaultHandler(metricsPath) : defaultHandler,
         authenticator,
         authenticatedSubjectAttributeName);
+    try {
+      server.removeContext(metricsPath);
+    } catch (IllegalArgumentException e) {
+      // context metricsPath not registered yet, ignore
+    }
     registerHandler(
         metricsPath,
         new MetricsHandler(config, registry),

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants